﻿using System.Collections.Generic;
using PMatch.Formats.sPMatch;
using PMatch.Formats.SequenceSample;
using System.Linq;
using System;

namespace PMatch.Sampling
{
	public class SampleWindow
	{
		public SampleWindow(int prefix, int sufix)
		{
			Prefix = prefix;
			Sufix = sufix;
		}
		public int Prefix { get; private set; }
		public int Sufix { get; private set; }		
		public int Length { get { return Prefix + Sufix; } }
		public static SampleWindow New(int prefix, int sufix)
		{
			return new SampleWindow(prefix, sufix);
		}

		public override string ToString()
		{
			return "[" + Prefix.ToString() + ":" + Sufix.ToString() + "]";
		}
	}

	public class Sample
	{
		public Sample(sequence origin, int site)
		{
			Origin = origin;
			Site = site;
		}
		public sequence Origin { get; private set; }
		public int Site { get; private set; }

		public string Read(int length)
		{
			return Origin.content.Substring(Site, length);
		}
	}

	public class Collision
	{
		public Collision(Sample org, Sample colliding, int size)
		{
			SampleOriginal = org;
			SampleColliding = colliding;
			Size = size;
		}
		public Sample SampleOriginal { get; private set; }
		public Sample SampleColliding { get; private set; }
		public int Size { get; private set; }
		public int GetOffset()
		{
			var offset = Math.Abs(SampleColliding.Site - SampleOriginal.Site);
			return offset;
		}
	}

	public class CollectSamplesResult
	{
		public Dictionary<string, Sample> PositiveSamples { get; set; }
		public Dictionary<string, Sample> NegativeSamples { get; set; }
		public List<Collision> CollisionsWithPositiveSet { get; set; }
		public List<Collision> CollisionsWithNegativeSet { get; set; }
		public SampleWindow Window { get; set; }
		public bool HasCollisions { get; set; }

		public int? GetMCOP()
		{
			if (CollisionsWithPositiveSet == null || CollisionsWithPositiveSet.Count == 0) return null;
			var mco = CollisionsWithPositiveSet.Max(x => x.GetOffset());
			return mco;
		}

		public int? GetMCON()
		{
			if (CollisionsWithNegativeSet == null || CollisionsWithNegativeSet.Count == 0) return null;
			var mco = CollisionsWithNegativeSet.Max(x => x.GetOffset());
			return mco;
		}
	}

	public static class SampleCollector
	{

		/// <summary>
		/// Aplica la ventana definida por <paramref name="prefixWindowSize"/> y <paramref name="sufixWindowSize"/>
		/// a las secuencias <paramref name="sequences"/> y construye los diccionarios de muestras negativas, positivas, y 
		/// los listados de colision
		/// </summary>
		/// <param name="breakOnCollision">Si se encuentra una colision detiene el procedimiento</param>
		/// <param name="siteIndex">numero del sitio de clivaje, Indice basado en cero</param>
		/// <returns><value>true</value> cuando se ha detectado al menos una colision</returns>
		public static CollectSamplesResult CollectSamples(IEnumerable<sequence> sequences,
			SampleWindow window,
			bool breakOnCollision, int siteIndex)
		{
			var positiveSamples = new Dictionary<string, Sample>();
			var negativeSamples = new Dictionary<string, Sample>();
			var positiveCollisions = new List<Collision>(); // Colisiones contra el conjunto de muestras positivas
			var negativeCollisions = new List<Collision>(); // Colisiones contra el conjunto de muestras negativas
			var result = new CollectSamplesResult();
			result.HasCollisions = CollectSamples(sequences, positiveSamples, negativeSamples, positiveCollisions, negativeCollisions, window, breakOnCollision, siteIndex);
			result.PositiveSamples = positiveSamples;
			result.NegativeSamples = negativeSamples;
			result.CollisionsWithPositiveSet = positiveCollisions;
			result.CollisionsWithNegativeSet = negativeCollisions;
			return result;
		}

		/// <summary>
		/// Aplica la ventana definida por <paramref name="prefixWindowSize"/> y <paramref name="sufixWindowSize"/>
		/// a la secuencia <paramref name="sequence"/> y construye los diccionarios de muestras negativas, positivas, y 
		/// los listados de colision
		/// </summary>		
		/// <param name="breakOnCollision">Si se encuentra una colision detiene el procedimiento</param>
		/// <param name="siteIndex">numero del sitio de clivaje, Indice basado en cero</param>
		/// <returns><value>true</value> cuando se ha detectado al menos una colision</returns>
		public static bool CollectSamples(sequence sequence,
			Dictionary<string, Sample> positiveSamples,
			Dictionary<string, Sample> negativeSamples,
			List<Collision> positiveCollisions, // Colisiones contra el conjunto de muestras positivas
			List<Collision> negativeCollisions, // Colisiones contra el conjunto de muestras negativas
			SampleWindow window,
			bool breakOnCollision, int siteIndex)
		{
			var startAfterSite = sequence.chain[siteIndex + 1].start;
			var positiveSampleBegin = startAfterSite - window.Prefix;
			var windowSize = window.Length;
			bool hasCollisions = false;

			var lastWindowPosition = sequence.content.Length - windowSize;
			for (int i = 0; i <= lastWindowPosition; i++)
			{
				bool localHasCollisions;
				if (i == positiveSampleBegin) // la muestra positiva se trata diferente
				{
					localHasCollisions = TryCollectSample(sequence, i, windowSize, positiveSamples, negativeSamples, negativeCollisions);
				}
				else
				{
					localHasCollisions = TryCollectSample(sequence, i, windowSize, negativeSamples, positiveSamples, positiveCollisions);
				}
				hasCollisions |= localHasCollisions;
				if (hasCollisions && breakOnCollision) return hasCollisions;
			}
			return hasCollisions;
		}

		/// <summary>
		/// Aplica la ventana definida por <paramref name="prefixWindowSize"/> y <paramref name="sufixWindowSize"/>
		/// a las secuencias <paramref name="sequences"/> y construye los diccionarios de muestras negativas, positivas, y 
		/// los listados de colision
		/// </summary>
		/// <param name="breakOnCollision">Si se encuentra una colision detiene el procedimiento</param>
		/// <param name="siteIndex">numero del sitio de clivaje, Indice basado en cero</param>
		/// <returns><value>true</value> cuando se ha detectado al menos una colision</returns>
		public static bool CollectSamples(IEnumerable<sequence> sequences,
			Dictionary<string, Sample> positiveSamples,
			Dictionary<string, Sample> negativeSamples,
			List<Collision> positiveCollisions, // Colisiones contra el conjunto de muestras positivas
			List<Collision> negativeCollisions, // Colisiones contra el conjunto de muestras negativas
			SampleWindow window,
			bool breakOnCollision, int siteIndex)
		{
			var hasCollisions = false;
			foreach (var sequence in sequences)
			{
				if (CollectSamples(sequence, positiveSamples, negativeSamples, positiveCollisions, negativeCollisions, window, breakOnCollision, siteIndex))
				{
					hasCollisions = true;
					if (breakOnCollision) break;
				}
			}
			return hasCollisions;
		}

		/// <summary>
		/// Procesa la muestra indicada por <paramref name="sampleStartIndex"/> y <paramref name="sampleSize"/>
		/// dentro de la secuencia <paramref name="sequence"/> identificando si se trata de una colision o no y
		/// realizando la accion que corresponda a cada caso.
		/// </summary>
		/// <param name="sequence">Secuencia donde se encuentra la muestra</param>
		/// <param name="sampleStartIndex">Indice donde empieza la muestra</param>
		/// <param name="sampleSize">Tamaño de la muestra</param>
		/// <param name="addTo">En caso de no colisionar la muestra se añade a este diccionario</param>
		/// <param name="checkCollisionsWith">Chequear si es colision probando si ya existe en este diccionario</param>
		/// <param name="collisions">Si es una colision reportarla a esta lista</param>		
		/// <returns><value>true</value> si se reportó como colision</returns>
		public static bool TryCollectSample(sequence sequence,
			int sampleStartIndex, int sampleSize,
			Dictionary<string, Sample> addTo,
			Dictionary<string, Sample> checkCollisionsWith,
			List<Collision> collisions)
		{
			Sample collidingOriginal;
			var testingSample = new Sample(sequence, sampleStartIndex);
			var str = testingSample.Read(sampleSize);
			if (checkCollisionsWith.TryGetValue(str, out collidingOriginal))
			{
				collisions.Add(new Collision(collidingOriginal, testingSample, sampleSize));
				return true;
			}
			else
			{
				if (!addTo.ContainsKey(str)) addTo.Add(str, testingSample);
				return false;
			}
		}

		/// <summary>
		/// Adjunta a <paramref name="dataset" /> las muestras en <paramref name="samples"/> etiquetandolas como positivas		
		/// </summary>		
		public static void AppendToDatasetAsPositive(Dictionary<string, Sample> samples, samples dataset)
		{
			if (dataset.positive == null) dataset.positive = new List<string>();
			dataset.positive.AddRange(samples.Keys);
		}

		/// <summary>
		/// Adjunta a <paramref name="dataset" /> las muestras en <paramref name="samples"/> etiquetandolas como negativas
		/// </summary>		
		public static void AppendToDatasetAsNegative(Dictionary<string, Sample> samples, samples dataset)
		{
			if (dataset.negative == null) dataset.negative = new List<string>();
			dataset.negative.AddRange(samples.Keys);
		}

		/// <summary>
		/// Construye un conjunto de datos a partir de los diccionarios de informacion sobre las muestras
		/// </summary>	
		public static samples CreateDataset(CollectSamplesResult result)
		{
			var ds = CreateDataset(result.PositiveSamples, result.NegativeSamples);
			return ds;
		}


		/// <summary>
		/// Construye un conjunto de datos a partir de los diccionarios de informacion sobre las muestras
		/// </summary>		
		public static samples CreateDataset(Dictionary<string, Sample> Psamples, Dictionary<string, Sample> Nsamples)
		{
			var dataset = new samples();
			AppendToDatasetAsPositive(Psamples, dataset);
			AppendToDatasetAsNegative(Nsamples, dataset);
			return dataset;
		}

		/// <summary>
		/// Resuelve las colisiones encontradas con el siguiente criterio:
		/// Si la colision es contra el conjunto negativo, se adopta la muestra que colisiona como positiva y se retira del
		/// conjunto de negativas.
		/// Si la colision es contra el conjunto positivo, se mantine la muestra que colisiona en el conjunto positivo y 
		/// retira del conjunto de negativas.
		/// </summary>
		/// <param name="negCollisions">Colisiones contra el conjunto negativo</param>
		/// <param name="posCollisions">Colisiones contra el conjunto positivo</param>
		/// <param name="posSamples">Muestras positivas</param>
		/// <param name="negSamples">Muestras negativas</param>
		public static void ResolveCollisions(List<Collision> negCollisions, List<Collision> posCollisions, Dictionary<string, Sample> posSamples, Dictionary<string, Sample> negSamples)
		{
			foreach (var collision in negCollisions)
			{
				var str = collision.SampleOriginal.Read(collision.Size);
				negSamples.Remove(str);
				if (!posSamples.ContainsKey(str))
				{
					posSamples.Add(str, collision.SampleColliding);
				}
			}
			foreach (var collision in posCollisions)
			{
				var str = collision.SampleOriginal.Read(collision.Size);
				var rem = negSamples.Remove(str);
			}
		}

		/// <summary>
		/// Resuelve las colisiones encontradas con el siguiente criterio:
		/// Si la colision es contra el conjunto negativo, se adopta la muestra que colisiona como positiva y se retira del
		/// conjunto de negativas.
		/// Si la colision es contra el conjunto positivo, se mantine la muestra que colisiona en el conjunto positivo y 
		/// retira del conjunto de negativas.
		/// </summary>
		/// <returns>The same object</returns>
		public static CollectSamplesResult ResolveCollisions(CollectSamplesResult samplesRes)
		{
			ResolveCollisions(samplesRes.CollisionsWithNegativeSet, samplesRes.CollisionsWithPositiveSet, samplesRes.PositiveSamples, samplesRes.NegativeSamples);
			return samplesRes;
		}
	}
}
